# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations

import ctypes
import gc
import os.path
import sys
import threading
import webbrowser
from typing import List, Union

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt


from VeraGridEngine.IO.file_system import get_create_veragrid_folder

# GUI imports
from PySide6 import QtGui, QtWidgets, QtCore

# Engine imports
from VeraGridEngine.Devices.multi_circuit import MultiCircuit
import VeraGridEngine.Simulations as sim
from VeraGridEngine.enumerations import EngineType, DeviceType, SimulationTypes
from VeraGridEngine.Compilers.circuit_to_data import compile_numerical_circuit_at
from VeraGridEngine.DataStructures.numerical_circuit import NumericalCircuit

from VeraGridEngine.Compilers.circuit_to_bentayga import BENTAYGA_AVAILABLE
from VeraGridEngine.Compilers.circuit_to_newton_pa import NEWTON_PA_AVAILABLE
from VeraGridEngine.Compilers.circuit_to_gslv import GSLV_AVAILABLE
from VeraGridEngine.Compilers.circuit_to_pgm import PGM_AVAILABLE
from VeraGridEngine.Simulations.Clustering.clustering_results import ClusteringResults

import VeraGrid.Gui.gui_functions as gf
import VeraGrid.Session.synchronization_driver as syncdrv
from VeraGrid.Gui.AboutDialogue.about_dialogue import AboutDialogueGuiGUI
from VeraGrid.Gui.Analysis.AnalysisDialogue import GridAnalysisGUI
from VeraGrid.Gui.ContingencyPlanner.contingency_planner_dialogue import ContingencyPlannerGUI
from VeraGrid.Gui.CoordinatesInput.coordinates_dialogue import CoordinatesInputGUI
from VeraGrid.Gui.general_dialogues import CheckListDialogue, StartEndSelectionDialogue
from VeraGrid.Gui.messages import yes_no_question, warning_msg, info_msg, error_msg
from VeraGrid.Gui.GridGenerator.grid_generator_dialogue import GridGeneratorGUI
from VeraGrid.Gui.LoadCatalogue.catalogue_dialogue import CatalogueGUI
from VeraGrid.Gui.Main.MainWindow import Ui_mainWindow, QMainWindow
from VeraGrid.Gui.Main.object_select_window import ObjectSelectWindow
from VeraGrid.Gui.ProfilesInput.models_dialogue import ModelsInputGUI
from VeraGrid.Gui.ProfilesInput.profile_dialogue import ProfileInputGUI
from VeraGrid.Session.session import SimulationSession, GcThread
from VeraGrid.Gui.SigmaAnalysis.sigma_analysis_dialogue import SigmaAnalysisGUI
from VeraGrid.Gui.SyncDialogue.sync_dialogue import SyncDialogueWindow
from VeraGrid.Gui.TowerBuilder.LineBuilderDialogue import TowerBuilderGUI
from VeraGrid.Gui.GridReduce.grid_reduce import GridReduceDialogue
from VeraGrid.Gui.RmsModelEditor.rms_model_editor_engine import RmsModelEditorGUI
from VeraGrid.Gui.Diagrams.SchematicWidget.diagram_bus_selection_dialogue import DiagramBusSelectorDialogue
from VeraGrid.Gui.rms_events_editor_dialog import RmsEventEditor
from VeraGrid.Gui.Diagrams.generic_graphics import IS_DARK
from VeraGrid.Gui.python_console import PythonConsole
from VeraGrid.Gui.toast_widget import ToastManager


def terminate_thread(thread):
    """
    Terminates a python thread from another thread.

    :param thread: a threading.Thread instance
    """
    if not thread.is_alive():
        return False

    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_long(thread.ident), exc)
    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")

    return True


def traverse_objects(name, obj, lst: list, i=0):
    """

    :param name:
    :param obj:
    :param lst:
    :param i:
    """
    lst.append((name, sys.getsizeof(obj)))
    if i < 10:
        if hasattr(obj, '__dict__'):
            for name2, obj2 in obj.__dict__.items():
                if isinstance(obj2, np.ndarray):
                    lst.append((name + "/" + name2, sys.getsizeof(obj2)))
                else:
                    if isinstance(obj2, list):
                        # list or
                        for k, obj3 in enumerate(obj2):
                            traverse_objects(name=name + "/" + name2 + '[' + str(k) + ']',
                                             obj=obj3, lst=lst, i=i + 1)
                    elif isinstance(obj2, dict):
                        # list or
                        for name3, obj3 in obj2.items():
                            traverse_objects(name=name + "/" + name2 + '[' + name3 + ']',
                                             obj=obj3, lst=lst, i=i + 1)
                    else:
                        # normal obj
                        if obj2 != obj:
                            traverse_objects(name=name + "/" + name2, obj=obj2, lst=lst, i=i + 1)


class BaseMainGui(QMainWindow):
    """
    DiagramFunctionsMain
    """

    def __init__(self, parent=None):
        """

        @param parent:
        """
        # create main window
        QMainWindow.__init__(self, parent)
        self.ui = Ui_mainWindow()
        self.ui.setupUi(self)

        # Declare circuit
        self.circuit: MultiCircuit = MultiCircuit()

        self.lock_ui = False
        self.ui.progress_frame.setVisible(self.lock_ui)

        self.stuff_running_now: List[SimulationTypes] = list()

        self.session: SimulationSession = SimulationSession(name='GUI session')

        self._file_name = ''

        self.project_directory = os.path.expanduser("~")

        self.current_boundary_set: str = ""

        # toast manager
        self.toast_manager = ToastManager(parent=self, position_top=False)

        # threads ------------------------------------------------------------------------------------------------------
        self.painter = None
        self.open_file_thread_object = None
        self.save_file_thread_object = None
        self.last_file_driver = None
        self.delete_and_reduce_driver = None
        self.export_all_thread_object = None
        self.topology_reduction = None
        self.find_node_groups_driver: Union[sim.NodeGroupsDriver, None] = None
        self.file_sync_thread = syncdrv.FileSyncThread(self.circuit, None, None)

        self.task_pool = QtCore.QThreadPool()

        # simulation start end
        self.simulation_start_index: int = -1
        self.simulation_end_index: int = -1

        # Add console --------------------------------------------------------------------------------------------------
        self.console = PythonConsole(banner="VeraGrid Python Console!")
        self.ui.consoleLayout.addWidget(self.console)

        # window pointers ----------------------------------------------------------------------------------------------
        self.file_sync_window: Union[SyncDialogueWindow, None] = None
        self.sigma_dialogue: Union[SigmaAnalysisGUI, None] = None
        self.grid_generator_dialogue: Union[GridGeneratorGUI, None] = None
        self.catalogue_dialogue: Union[CatalogueGUI, None] = None
        self.contingency_planner_dialogue: Union[ContingencyPlannerGUI, None] = None
        self.analysis_dialogue: Union[GridAnalysisGUI, None] = None
        self.profile_input_dialogue: Union[ProfileInputGUI, None] = None
        self.models_input_dialogue: Union[ModelsInputGUI, None] = None
        self.object_select_window: Union[ObjectSelectWindow, None] = None
        self.coordinates_window: Union[CoordinatesInputGUI, None] = None
        self.about_msg_window: Union[AboutDialogueGuiGUI, None] = None
        self.tower_builder_window: Union[TowerBuilderGUI, None] = None
        self.rms_model_Editor_window: Union[RmsModelEditorGUI, None] = None
        self.rms_events_Editor_window: Union[RmsEventEditor, None] = None
        self.investment_checks_diag: Union[CheckListDialogue, None] = None
        self.new_se_dlg: Union[CheckListDialogue, None] = None
        self.contingency_checks_diag: Union[CheckListDialogue, None] = None
        self.ra_checks_diag: Union[CheckListDialogue, None] = None
        self.start_end_dialogue_window: Union[StartEndSelectionDialogue, None] = None
        self.grid_reduction_dialogue: GridReduceDialogue | None = None
        self.select_bus_dlg: DiagramBusSelectorDialogue | None = None

        # available engines --------------------------------------------------------------------------------------------
        engine_lst = [EngineType.VeraGrid]
        if GSLV_AVAILABLE:
            engine_lst.append(EngineType.GSLV)
        if NEWTON_PA_AVAILABLE:
            engine_lst.append(EngineType.NewtonPA)
        if BENTAYGA_AVAILABLE:
            engine_lst.append(EngineType.Bentayga)
        if PGM_AVAILABLE:
            engine_lst.append(EngineType.PGM)

        self.ui.engineComboBox.setModel(gf.get_list_model([x.value for x in engine_lst]))
        self.ui.engineComboBox.setCurrentIndex(0)
        self.engine_dict = {x.value: x for x in engine_lst}

        # available engines --------------------------------------------------------------------------------------------
        exchange_places = [DeviceType.AreaDevice, DeviceType.CountryDevice]
        exchange_places_mdl = gf.get_list_model([x.value for x in exchange_places])
        self.ui.fromComboBox.setModel(exchange_places_mdl)
        self.ui.fromComboBox.setCurrentIndex(0)
        self.ui.toComboBox.setModel(exchange_places_mdl)
        self.ui.toComboBox.setCurrentIndex(0)
        self.exchange_places_dict = {x.value: x for x in exchange_places}

        # dark mode detection ------------------------------------------------------------------------------------------
        self.ui.dark_mode_checkBox.setChecked(IS_DARK)

        self.calculation_inputs_to_display = None

        # --------------------------------------------------------------------------------------------------------------
        self.ui.actionClear_stuff_running_right_now.triggered.connect(self.clear_stuff_running)
        self.ui.actionAbout.triggered.connect(self.about_box)
        self.ui.actionAuto_rate_branches.triggered.connect(self.auto_rate_branches)
        self.ui.actionDetect_transformers.triggered.connect(self.detect_transformers)
        self.ui.actionLaunch_data_analysis_tool.triggered.connect(self.display_grid_analysis)
        self.ui.actionOnline_documentation.triggered.connect(self.show_online_docs)
        self.ui.actionReport_a_bug.triggered.connect(self.report_a_bug)

        self.ui.actionFix_generators_active_based_on_the_power.triggered.connect(
            self.fix_generators_active_based_on_the_power
        )
        self.ui.actionFix_loads_active_based_on_the_power.triggered.connect(self.fix_loads_active_based_on_the_power)
        self.ui.actionInitialize_contingencies.triggered.connect(self.initialize_contingencies)

        # Buttons
        self.ui.cancelButton.clicked.connect(self.set_cancel_state)

        # doubleSpinBox
        self.ui.fbase_doubleSpinBox.valueChanged.connect(self.change_circuit_base)
        self.ui.sbase_doubleSpinBox.valueChanged.connect(self.change_circuit_base)

        self.ui.grid_name_line_edit.textChanged.connect(self.change_circuit_name)

        # combo-boxes
        self.ui.fromComboBox.currentTextChanged.connect(self.update_from_to_list_views)
        self.ui.toComboBox.currentTextChanged.connect(self.update_from_to_list_views)

    def LOCK(self, val: bool = True) -> None:
        """
        Lock the interface to prevent new simulation launches
        :param val:
        :return:
        """
        self.lock_ui = val
        self.ui.progress_frame.setVisible(self.lock_ui)
        QtGui.QGuiApplication.processEvents()

    def UNLOCK(self) -> None:
        """
        Unlock the interface
        """
        if not self.any_thread_running():
            self.LOCK(False)

    @property
    def file_name(self) -> str:
        """
        Get the current file name
        :return: str
        """
        return self._file_name

    @file_name.setter
    def file_name(self, val: str):
        """
        Set the current file name
        :param val: file name
        """
        self._file_name = val
        if not isinstance(self._file_name, list):
            self.ui.file_information_label.setText(self._file_name)
        else:
            self.ui.file_information_label.setText("")

    @staticmethod
    def collect_memory() -> None:
        """
        Collect memory
        """
        for i in (0, 1, 2):
            gc.collect(generation=i)

    def get_simulation_threads(self) -> List[GcThread]:
        """
        Get all threads that has to do with simulation
        :return: list of simulation threads
        """

        all_threads = list(self.session.threads.values())

        return all_threads

    def get_process_threads(self) -> List[GcThread]:
        """
        Get all threads that has to do with processing
        :return: list of process threads
        """
        all_threads = [self.open_file_thread_object,
                       self.save_file_thread_object,
                       self.painter,
                       self.delete_and_reduce_driver,
                       self.export_all_thread_object,
                       self.find_node_groups_driver,
                       self.file_sync_thread,
                       ]
        return all_threads

    def get_all_threads(self) -> List[GcThread]:
        """
        Get all threads
        :return: list of all threads
        """
        all_threads = self.get_simulation_threads() + self.get_process_threads()
        return all_threads

    def stop_all_threads(self):
        """
        Stop all running threads
        """
        for thr in self.get_all_threads():
            if thr is not None:
                thr.quit()

        for thread in threading.enumerate():
            print(thread.name, end="")
            if "MainThread" not in thread.name:
                stat = terminate_thread(thread)
                if stat:
                    print(" killed")
                else:
                    print(" not killed")
            else:
                print(" Skipped")

        # second pass, kill main too
        for thread in threading.enumerate():
            print(thread.name, end="")
            stat = terminate_thread(thread)
            if stat:
                print(" killed")
            else:
                print(" not killed")

    def any_thread_running(self) -> bool:
        """
        Checks if any thread is running
        :return: True/False
        """
        val = False

        # this list cannot be created only once, because the None will be copied
        # instead of being a pointer to the future value like it would in a typed language
        all_threads = self.get_all_threads()

        for thr in all_threads:
            if thr is not None:
                if thr.isRunning():
                    return True
        return val

    def clear_stuff_running(self) -> None:
        """
        This clears the list of stuff running right now
        this list blocks new executions of the same threads.
        Cleaning is useful if a particular thread crashes and you want to retry.
        """
        self.stuff_running_now.clear()

    def get_all_objects_in_memory(self):
        """
        Get a list of the objects in memory
        :return:
        """
        objects = []
        # for name, obj in globals().items():
        #     objects.append([name, sys.getsizeof(obj)])

        traverse_objects('MainGUI', self, objects)

        df = pd.DataFrame(data=objects, columns=['Name', 'Size (kb)'])
        df.sort_values(by='Size (kb)', inplace=True, ascending=False)
        return df

    def expand_object_tree_nodes(self) -> None:
        """
        Expand objects' tree nodes
        """
        proxy = self.ui.dataStructuresTreeView.model()

        for row in range(proxy.rowCount()):
            index = proxy.index(row, 0)
            self.ui.dataStructuresTreeView.expand(index)

    def get_simulation_start(self) -> int:
        """
        Get the start simulation index
        """
        return self.simulation_start_index

    def get_simulation_end(self) -> int:
        """
        Get the end simulation index
        """
        return self.simulation_end_index

    def setup_sim_indices(self, st: int, en: int):
        """
        Set the simulation indices
        :param st: start index
        :param en: end index
        """
        self.simulation_start_index = st
        self.simulation_end_index = en

    def get_diagram_slider_index(self) -> Union[int, None]:
        """
        Get the diagram slider value
        :return: [None, int]
        """
        idx = self.ui.diagram_step_slider.value()
        return idx if idx > -1 else None

    def get_db_slider_index(self) -> Union[int, None]:
        """
        Get the db slider value
        :return: [None, int]
        """
        idx = self.ui.db_step_slider.value()
        return idx if idx > -1 else None

    def setup_time_sliders(self):
        """
        Setup the time sliders
        """
        for slider in [self.ui.diagram_step_slider,
                       self.ui.db_step_slider,
                       self.ui.compiled_arrays_step_slider]:
            if self.circuit.has_time_series:
                slider.setRange(-1, self.circuit.get_time_number() - 1)
            else:
                slider.setRange(-1, -1)
            slider.setValue(-1)

    def update_date_dependent_combos(self):
        """
        update the drop down menus that display dates
        """
        if self.circuit.time_profile is not None:
            mdl = gf.get_list_model([str(e) for e in self.circuit.time_profile])
            # setup profile sliders
            t = len(self.circuit.time_profile) - 1
            self.setup_sim_indices(0, t)

        else:
            mdl = QtGui.QStandardItemModel()
            self.setup_sim_indices(-1, -1)
        self.ui.vs_departure_comboBox.setModel(mdl)
        self.ui.vs_target_comboBox.setModel(mdl)
        self.setup_time_sliders()

    def update_from_to_list_views(self):
        """
        Update the exchange area, countries, etc... dependent combos
        """
        same_type = self.ui.fromComboBox.currentText() == self.ui.toComboBox.currentText()

        devs_from = self.circuit.get_elements_by_type(self.exchange_places_dict[self.ui.fromComboBox.currentText()])
        mdl1 = gf.get_list_model([str(elm) for elm in devs_from], checks=True)
        self.ui.fromListView.setModel(mdl1)

        devs_to = self.circuit.get_elements_by_type(self.exchange_places_dict[self.ui.toComboBox.currentText()])
        mdl2 = gf.get_list_model([str(elm) for elm in devs_to], checks=True)
        self.ui.toListView.setModel(mdl2)

        if len(devs_from) > 1 and same_type:
            self.ui.fromListView.model().item(0).setCheckState(QtCore.Qt.CheckState.Checked)
            self.ui.toListView.model().item(1).setCheckState(QtCore.Qt.CheckState.Checked)

    def fix_generators_active_based_on_the_power(self, ask_before=True):
        """
        set the generators active based on the active power values
        :return:
        """

        if ask_before:
            ok = yes_no_question("This action sets the generation active profile based on the active power profile "
                                 "such that ig a generator active power is zero, the active value is false",
                                 "Set generation active profile")
        else:
            ok = True

        if ok:
            self.circuit.set_generators_active_profile_from_their_active_power()
            self.circuit.set_batteries_active_profile_from_their_active_power()

    def fix_loads_active_based_on_the_power(self, ask_before=True):
        """
        set the loads active based on the active power values
        :return:
        """

        if ask_before:
            ok = yes_no_question("This action sets the generation active profile based on the active power profile "
                                 "such that ig a generator active power is zero, the active value is false",
                                 "Set generation active profile")
        else:
            ok = True

        if ok:
            self.circuit.set_loads_active_profile_from_their_active_power()

    def get_preferred_engine(self) -> EngineType:
        """
        Get the currently selected engine
        :return: EngineType
        """
        val = self.ui.engineComboBox.currentText()
        return self.engine_dict[val]

    def about_box(self):
        """
        Display about box
        :return:
        """

        self.about_msg_window = AboutDialogueGuiGUI(self)
        self.about_msg_window.setVisible(True)

    @staticmethod
    def show_online_docs():
        """
        Open the online documentation in a web browser
        """
        webbrowser.open('https://veragrid.readthedocs.io/en/latest/', new=2)

    @staticmethod
    def report_a_bug():
        """
        Open the online github issues in a web browser
        """
        webbrowser.open('https://github.com/SanPen/VeraGrid/issues', new=2)

    def auto_rate_branches(self):
        """
        Rate the Branches that do not have rate
        """

        branches = self.circuit.get_branches()

        if len(branches) > 0:
            _, pf_results = self.session.power_flow

            if pf_results is not None:
                factor = self.ui.branch_rating_doubleSpinBox.value()

                for i, branch in enumerate(branches):

                    S = pf_results.Sf[i]

                    if branch.rate < 1e-3 or self.ui.rating_override_checkBox.isChecked():
                        r = np.round(abs(S) * factor, 1)
                        branch.rate = r if r > 0.0 else 1.0
                    else:
                        pass  # the rate is ok

            else:
                info_msg('Run a power flow simulation first.\nThe results are needed in this function.')

        else:
            warning_msg('There are no Branches!')

    def detect_transformers(self):
        """
        Detect which Branches are transformers
        """
        if len(self.circuit.lines) > 0:

            for elm in self.circuit.lines:

                v1 = elm.bus_from.Vnom
                v2 = elm.bus_to.Vnom

                if abs(v1 - v2) > 1.0:
                    self.circuit.convert_line_to_transformer(elm)
                else:

                    pass  # is a line

        else:
            warning_msg('There are no Branches!')

    def set_cancel_state(self) -> None:
        """
        Cancel what ever's going on that can be cancelled
        @return:
        """

        reply = QtWidgets.QMessageBox.question(self, 'Message',
                                               'Are you sure that you want to cancel the simulation?',
                                               QtWidgets.QMessageBox.StandardButton.Yes,
                                               QtWidgets.QMessageBox.StandardButton.No)

        if reply == QtWidgets.QMessageBox.StandardButton.Yes.value:
            # send the cancel state to whatever it is being executed

            for drv in self.get_all_threads():
                if drv is not None:
                    if hasattr(drv, 'cancel'):
                        drv.cancel()
        else:
            pass

    def display_grid_analysis(self):
        """
        Display the grid analysis GUI
        """

        self.analysis_dialogue = GridAnalysisGUI(circuit=self.circuit)

        self.analysis_dialogue.resize(int(1.61 * 600.0), 600)
        self.analysis_dialogue.show()

    def change_circuit_base(self):
        """
        Update the circuit base values from the UI
        """

        Sbase_new = self.ui.sbase_doubleSpinBox.value()
        self.circuit.change_base(Sbase_new)

        self.circuit.fBase = self.ui.fbase_doubleSpinBox.value()

    def change_circuit_name(self):
        """

        :return:
        """
        self.circuit.name = self.ui.grid_name_line_edit.text().strip()

    def get_snapshot_circuit(self):
        """
        Get a snapshot compilation
        :return: SnapshotData instance
        """
        return compile_numerical_circuit_at(circuit=self.circuit)

    @property
    def numerical_circuit(self) -> NumericalCircuit:
        """
        get the snapshot NumericalCircuit
        :return: NumericalCircuit
        """
        return self.get_snapshot_circuit()

    @property
    def islands(self) -> List[NumericalCircuit]:
        """
        get the snapshot islands
        :return: List[NumericalCircuit]
        """
        numerical_circuit = compile_numerical_circuit_at(circuit=self.circuit)
        calculation_inputs = numerical_circuit.split_into_islands()
        return calculation_inputs

    def initialize_contingencies(self):
        """
        Launch the contingency planner to initialize the contingencies
        :return:
        """
        self.contingency_planner_dialogue = ContingencyPlannerGUI(parent=self, grid=self.circuit)
        self.contingency_planner_dialogue.exec()

        # gather results
        if self.contingency_planner_dialogue.generated_results:
            if len(self.contingency_planner_dialogue.contingency_groups):
                self.circuit.contingency_groups += self.contingency_planner_dialogue.contingency_groups
                self.circuit.contingencies += self.contingency_planner_dialogue.contingencies
            else:
                info_msg(text="No contingencies were generated :/", title="Contingency planner")

    def clear_console(self):
        """
        Clear console output
        """
        self.console.clear()

    def reset_console(self):
        """
        Reset console
        :return:
        """
        self.console.reset()
        self.add_console_vars()

    def print_console_help(self):

        self.console.append_output("")
        self.console.append_output('VeraGrid internal commands.\n')
        self.console.append_output('If a command is unavailable is because the study has not been executed yet.')

        self.console.append_output('\n\nclc():\tclear the console.')

        self.console.append_output('\n\nApp functions:')
        self.console.append_output('\tapp.new_project(): Clear all.')
        self.console.append_output('\tapp.open_file(): Prompt to load VeraGrid compatible file')
        self.console.append_output('\tapp.save_file(): Prompt to save VeraGrid file')
        self.console.append_output('\tapp.export_diagram(): Prompt to export the diagram in png.')
        self.console.append_output(
            '\tapp.create_schematic_from_api(): Create the schematic from the circuit information.')
        self.console.append_output(
            '\tapp.adjust_all_node_width(): Adjust the width of all the nodes according to their name.')
        self.console.append_output('\tapp.numerical_circuit: get compilation of the assets.')
        self.console.append_output('\tapp.islands: get compilation of the assets split into the topological islands.')

        self.console.append_output('\n\nCircuit functions:')
        self.console.append_output(
            '\tapp.circuit.plot_graph(): Plot a graph in a Matplotlib window. Call plt.show() after.')

        self.console.append_output('\n\nPower flow results:')
        self.console.append_output('\tapp.session.power_flow.voltage:\t the nodal voltages in per unit')
        self.console.append_output('\tapp.session.power_flow.current:\t the branch currents in per unit')
        self.console.append_output('\tapp.session.power_flow.loading:\t the branch loading in %')
        self.console.append_output('\tapp.session.power_flow.losses:\t the branch losses in per unit')
        self.console.append_output('\tapp.session.power_flow.power:\t the nodal power Injections in per unit')
        self.console.append_output(
            '\tapp.session.power_flow.Sf:\t the branch power Injections in per unit at the "from" side')
        self.console.append_output(
            '\tapp.session.power_flow.St:\t the branch power Injections in per unit at the "to" side')

        self.console.append_output('\n\nShort circuit results:')
        self.console.append_output('\tapp.session.short_circuit.voltage:\t the nodal voltages in per unit')
        self.console.append_output('\tapp.session.short_circuit.current:\t the branch currents in per unit')
        self.console.append_output('\tapp.session.short_circuit.loading:\t the branch loading in %')
        self.console.append_output('\tapp.session.short_circuit.losses:\t the branch losses in per unit')
        self.console.append_output('\tapp.session.short_circuit.power:\t the nodal power Injections in per unit')
        self.console.append_output(
            '\tapp.session.short_circuit.power_from:\t the branch power Injections in per unit at the "from" side')
        self.console.append_output(
            '\tapp.session.short_circuit.power_to:\t the branch power Injections in per unit at the "to" side')
        self.console.append_output(
            '\tapp.session.short_circuit.short_circuit_power:\t Short circuit power in MVA of the grid nodes')

        self.console.append_output('\n\nOptimal power flow results:')
        self.console.append_output('\tapp.session.optimal_power_flow.voltage:\t the nodal voltages angles in rad')
        self.console.append_output('\tapp.session.optimal_power_flow.load_shedding:\t the branch loading in %')
        self.console.append_output('\tapp.session.optimal_power_flow.losses:\t the branch losses in per unit')
        self.console.append_output('\tapp.session.optimal_power_flow.Sbus:\t the nodal power Injections in MW')
        self.console.append_output('\tapp.session.optimal_power_flow.Sf:\t the branch power Sf')

        self.console.append_output('\n\nTime series power flow results:')
        self.console.append_output(
            '\tapp.session.power_flow_ts.time:\t Profiles time index (pandas DateTimeIndex object)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.load_profiles:\t Load profiles matrix (row: time, col: node)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.gen_profiles:\t Generation profiles matrix (row: time, col: node)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.voltages:\t nodal voltages results matrix (row: time, col: node)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.currents:\t Branches currents results matrix (row: time, col: branch)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.loadings:\t Branches loadings results matrix (row: time, col: branch)')
        self.console.append_output(
            '\tapp.session.power_flow_ts.losses:\t Branches losses results matrix (row: time, col: branch)')

        self.console.append_output('\n\nVoltage stability power flow results:')
        self.console.append_output(
            '\tapp.session.continuation_power_flow.voltage:\t Voltage values for every power multiplication factor.')
        self.console.append_output(
            '\tapp.session.continuation_power_flow.lambda:\t Value of power multiplication factor applied')
        self.console.append_output(
            '\tapp.session.continuation_power_flow.Sf:\t Power values for every power multiplication factor.')

        self.console.append_output('\n\nMonte Carlo power flow results:')
        self.console.append_output('\tapp.session.stochastic_power_flow.V_avg:\t nodal voltage average result.')
        self.console.append_output('\tapp.session.stochastic_power_flow.I_avg:\t branch current average result.')
        self.console.append_output('\tapp.session.stochastic_power_flow.Loading_avg:\t branch loading average result.')
        self.console.append_output('\tapp.session.stochastic_power_flow.Losses_avg:\t branch losses average result.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.V_std:\t nodal voltage standard deviation result.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.I_std:\t branch current standard deviation result.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.Loading_std:\t branch loading standard deviation result.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.Losses_std:\t branch losses standard deviation result.')
        self.console.append_output('\tapp.session.stochastic_power_flow.V_avg_series:\t nodal voltage average series.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.V_std_series:\t branch current standard deviation series.')
        self.console.append_output(
            '\tapp.session.stochastic_power_flow.error_series:\t Monte Carlo error series (the convergence value).')
        self.console.append_output('The same for app.latin_hypercube_sampling')

    def add_console_vars(self):
        """
        Add vars to the console
        :return:
        """
        self.console.add_var("hlp", self.print_console_help)
        self.console.add_var("np", np)
        self.console.add_var("pd", pd)
        self.console.add_var("plt", plt)
        self.console.add_var("clc", self.clear_console)
        self.console.add_var('app', self)
        self.console.add_var('circuit', self.circuit)
        self.console.add_var('user_folder', get_create_veragrid_folder)

    def show_toast(self, message: str, duration: int = 2000):
        """
        Show generic toast
        :param message: Message to display
        :param duration: duration in ms
        """
        self.toast_manager.show_toast(message=message, duration=duration, toast_type="veragrid")

    def show_error_toast(self, message: str, duration: int = 2000):
        """
        Show error toast
        :param message: Message to display
        :param duration: duration in ms
        """
        self.toast_manager.show_error_toast(message=message, duration=duration)

    def show_warning_toast(self, message: str, duration: int = 2000):
        """
        Show warning toast
        :param message: Message to display
        :param duration: duration in ms
        """
        self.toast_manager.show_warning_toast(message=message, duration=duration)

    def show_info_toast(self, message: str, duration: int = 2000):
        """
        Show info toast
        :param message: Message to display
        :param duration: duration in ms
        """
        self.toast_manager.show_info_toast(message=message, duration=duration)

    def get_clustering_results(self) -> Union[ClusteringResults, None]:
        """
        Get the clustering results if available
        :return: ClusteringResults or None
        """
        if self.ui.actionUse_clustering.isChecked():
            _, clustering_results = self.session.clustering

            if clustering_results is not None:
                n = len(clustering_results.time_indices)

                if n != self.ui.cluster_number_spinBox.value():
                    error_msg("The number of clusters in the stored results is different from the specified :(\n"
                              "Run another clustering analysis.")

                    return None
                else:
                    # all ok
                    return clustering_results
            else:
                # no results ...
                self.show_warning_toast("There are no clustering results.")
                self.ui.actionUse_clustering.setChecked(False)
                return None

        else:
            # not marked ...
            return None
